HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { GraphWorkbench } from '@/components/graph/graph-workbench';5import { defaultModeFor, GRAPH_MODES, graphSlugHref, isGraphMode } from '@/components/graph/modes';6import { BreadcrumbLd, Breadcrumbs } from '@/components/meta/breadcrumb-ld';7import { EntityBadge } from '@/components/ui/badges';8import { EntityLink } from '@/components/ui/entity';9import { Container, Note, PageHeader } from '@/components/ui/section';10import { EmptyState, Unavailable } from '@/components/ui/unavailable';11import { api, ApiError, apiD3, safe } from '@/lib/api';12import { fmtInt } from '@/lib/format';13import { predicateLabel, routes, SITE_NAME, SITE_URL, typeLabel } from '@/lib/site';14import type { ExploreNode, GraphExploreMode, GraphExplorePayload } from '@/lib/types';1516type Params = { params: Promise<{ slug: string }>; searchParams: Promise<{ depth?: string; mode?: string }> };17const LIMIT = 150;1819const hrefFor = graphSlugHref;2021/** 404 → notFound(); any other failure → null (the page renders an Unavailable state). */22async function loadGraph(slug: string, mode: GraphExploreMode, depth: 1 | 2): Promise<GraphExplorePayload | null> {23 try {24 return await apiD3.graphExplore(slug, mode, depth, LIMIT);25 } catch (e) {26 if (e instanceof ApiError && e.notFound) notFound();27 return null;28 }29}3031export async function generateMetadata({ params, searchParams }: Params): Promise<Metadata> {32 const { slug } = await params;33 const sp = await searchParams;34 const d = await safe(api.entity(slug));35 if (!d) return { title: 'Graph', robots: { index: false } };36 const mode = isGraphMode(sp.mode) ? sp.mode : defaultModeFor(d.entity_type);37 const depth: 1 | 2 = sp.depth === '2' ? 2 : 1;38 const modeLabel = GRAPH_MODES.find((x) => x.mode === mode)?.label ?? 'Graph';39 const title = `${d.name} — ${modeLabel.toLowerCase()} graph`;40 const og = `${SITE_URL}/graph/og?node=${encodeURIComponent(d.slug)}&mode=${mode}&depth=${depth}`;41 return { title, description: `Everything AI Atlas links to ${d.name} (${typeLabel(d.entity_type).toLowerCase()}) in ${modeLabel.toLowerCase()} mode, with the predicate of each relation.`, alternates: { canonical: hrefFor(d.slug, mode, depth) }, openGraph: { title: `${title} | ${SITE_NAME}`, type: 'article', images: [{ url: og, width: 1200, height: 630 }] }, twitter: { card: 'summary_large_image', images: [og] } };42}4344export default async function GraphPage({ params, searchParams }: Params) {45 const { slug } = await params;46 const sp = await searchParams;47 const d = await safe(api.entity(slug));48 if (!d) {49 // distinguish "unknown slug" (404) from "API down"50 try {51 await api.entity(slug);52 } catch (e) {53 if (e instanceof ApiError && e.notFound) notFound();54 }55 }56 const mode = isGraphMode(sp.mode) ? sp.mode : defaultModeFor(d?.entity_type);57 const depth: 1 | 2 = sp.depth === '2' ? 2 : 1;58 const graph = await loadGraph(slug, mode, depth);59 const rootId = graph?.root ?? d?.id ?? '';60 const nodes = graph?.nodes ?? [];61 const edges = graph?.edges ?? [];62 const byId = new Map(nodes.map((n) => [n.id, n]));63 const groups = new Map<string, { node: ExploreNode; direction: 'out' | 'in' }[]>();64 for (const e of edges) {65 const isOut = e.source === rootId;66 const isIn = e.target === rootId;67 if (!isOut && !isIn) continue;68 const other = byId.get(isOut ? e.target : e.source);69 if (!other) continue;70 const key = `${e.predicate}|${isOut ? 'out' : 'in'}`;71 (groups.get(key) ?? groups.set(key, []).get(key)!).push({ node: other, direction: isOut ? 'out' : 'in' });72 }73 const modeDef = GRAPH_MODES.find((x) => x.mode === mode)!;74 const crumbs = [{ name: SITE_NAME, href: '/' }, { name: 'Knowledge graph', href: '/graph' }, ...(d ? [{ name: typeLabel(d.entity_type, true), href: routes.listing(d.entity_type) }, { name: d.name, href: routes.entity(d) }] : []), { name: modeDef.label, href: hrefFor(slug, mode, depth) }];7576 return (77 <>78 <BreadcrumbLd items={crumbs} />79 <Container wide>80 <Breadcrumbs items={crumbs} />81 <PageHeader82 eyebrow={<>Graph explorer {d && <EntityBadge type={d.entity_type} small />}</>}83 title={d ? <>{modeDef.label} around <Link href={routes.entity(d)} className="hover:text-accent">{d.name}</Link></> : `Around ${slug}`}84 lede={d ? `${modeDef.hint[0]?.toUpperCase()}${modeDef.hint.slice(1)}${depth === 2 ? ' — two hops' : ''}. Click a node to inspect it, double-click to expand, drag to pan, wheel to zoom.` : undefined}85 aside={86 graph ? (87 <p className="tnum text-xs text-ink-3">88 {fmtInt(graph.counts?.nodes ?? nodes.length)} nodes · {fmtInt(graph.counts?.edges ?? edges.length)} edges{graph.truncated ? <span className="text-warning"> · truncated at {LIMIT}</span> : ''}89 </p>90 ) : undefined91 }92 className="pb-3"93 >94 <ul className="no-scrollbar -mx-4 mt-4 flex gap-1 overflow-x-auto px-4 md:mx-0 md:flex-wrap md:px-0" aria-label="Mode">95 {GRAPH_MODES.map((m) => (96 <li key={m.mode} className="shrink-0">97 <Link href={hrefFor(slug, m.mode, depth)} className={`inline-flex h-9 items-center border px-2.5 text-[12px] uppercase tracking-wide ${m.mode === mode ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink'}`} aria-current={m.mode === mode ? 'true' : undefined} title={m.hint}>98 {m.label}99 </Link>100 </li>101 ))}102 </ul>103 </PageHeader>104 </Container>105 <div className="pb-16">106 {!graph || !d ? (107 <Container>108 <Unavailable what="Graph" />109 </Container>110 ) : nodes.length <= 1 || edges.length === 0 ? (111 <Container>112 <EmptyState title={`No ${modeDef.label.toLowerCase()} relations recorded for ${d.name}`}>113 Relations are written only when a source states them. Try another mode above, or <Link href={routes.entity(d)} className="link">go back to {d.name} →</Link>114 </EmptyState>115 </Container>116 ) : (117 <>118 <GraphWorkbench key={`${slug}:${mode}:${depth}`} initial={graph} root={{ slug: d.slug, name: d.name, entity_type: d.entity_type }} mode={mode} depth={depth} urlStyle="path" embedded />119 <Container wide>120 <section className="mt-8 min-w-0">121 <p className="eyebrow mb-2">Direct relations of {d.name}, as a list</p>122 {groups.size === 0 ? (123 <p className="text-sm text-ink-3">No direct relations for the root node in this graph.</p>124 ) : (125 <dl className="kv">126 {[...groups.entries()].map(([key, items]) => {127 const [pred, dir] = key.split('|') as [string, 'out' | 'in'];128 return (129 <div key={key}>130 <dt>131 {predicateLabel(pred, dir)} <span className="tnum text-ink-3">{fmtInt(items.length)}</span>132 </dt>133 <dd className="flex flex-wrap gap-x-3 gap-y-1">134 {items.map(({ node }) => (135 <span key={node.id} className="inline-flex items-center gap-1.5">136 <EntityBadge type={node.entity_type} small />137 <EntityLink e={node} />138 </span>139 ))}140 </dd>141 </div>142 );143 })}144 </dl>145 )}146 </section>147 {graph.truncated && <Note className="mt-3">The neighbourhood is larger than {LIMIT} nodes; the API returned the first {LIMIT} and flagged the cut. Expand individual nodes, or use the entity's Relations block for full lists.</Note>}148 </Container>149 </>150 )}151 </div>152 </>153 );154}155